/*
 * Copyright (C) 2010 Fores Inc.
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package jp.fores.midori.server;

import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.Map.Entry;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import jp.fores.midori.core.util.ClassUtil;
import jp.fores.midori.core.util.StringUtil;
import jp.fores.midori.server.config.MidoriConfig;
import jp.fores.midori.server.config.MidoriConfigManager;
import jp.fores.midori.server.container.Container;
import jp.fores.midori.server.exception.RPCTargetServiceNotFoundException;
import jp.fores.midori.server.plugin.RPCPlugin;
import jp.fores.midori.server.plugin.RPCPluginUtil;

import org.apache.commons.io.FilenameUtils;
import org.apache.commons.io.IOUtils;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;

/**
 * RPC用のサーブレットクラス。<br>
 */
public class RPCServlet extends HttpServlet {
	//==========================================================
	//定数

	/**
	 * シリアルバージョンID
	 * (少しでも処理が軽くなるようにするために定義しておく)
	 */
	private static final long serialVersionUID = 1L;

	/**
	 * ログ出力用
	 */
	private static final Log log = LogFactory.getLog(RPCServlet.class);


	//==========================================================
	//フィールド

	/**
	 * 初期化処理が正常に完了したかどうかのフラグ
	 */
	protected boolean isInit = false;

	/**
	 * コンテナ
	 */
	protected Container container = null;

	/**
	 * プラグインのMap
	 */
	protected Map<String, RPCPlugin> pluginMap = new HashMap<String, RPCPlugin>();

	/**
	 * リスティング機能を有効にするかどうかのフラグ
	 */
	//(デフォルトはfalse)
	protected boolean isListing = false;


	//==========================================================
	//メソッド

	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//オーバーライドするメソッド

	/**
	 * 初期化処理を行います。<br>
	 *
	 * @param config サーブレットコンフィグ
	 * @throws ServletException サーブレット例外
	 */
	@Override
	public void init(ServletConfig config) throws ServletException {
		//==========================================================
		//親クラスの処理を先に呼び出す
		super.init(config);


		try {
			//==========================================================
			//設定ファイルの読み込み

			//サーブレットの初期化パラメーターから設定ファイルのパスを取得
			String configFile = config.getInitParameter("configFile");

			//取得できなかった場合
			if ((configFile == null) || "".equals(configFile)) {
				//設定ファイルのパスにデフォルトの値を設定
				configFile = "midori.xml";
			}

			//デバッグログが有効な場合
			if (log.isDebugEnabled()) {
				//デバッグログ出力
				log.debug("設定ファイルを読み込みます。:" + configFile);
			}

			//設定ファイルの入力ストリームを取得
			//(クラスパスの配下にあるファイルなので、クラスローダーから読み込む)
			InputStream is = getResourceAsStream(configFile);

			//入力ストリームが取得できなかった場合
			if (is == null) {
				//ファイルが見つからなかった場合の例外を投げる
				throw new FileNotFoundException("設定ファイルの読み込みに失敗しました。:"
						+ configFile);
			}

			try {
				//取得した入力ストリームの内容を元にして、設定内容を管理するクラスを初期化する
				MidoriConfigManager.getInstance().init(is);
			}
			//終了処理
			finally {
				//入力ストリームを確実に閉じる
				IOUtils.closeQuietly(is);
			}

			//設定内容を管理するクラスから設定内容を取得する
			MidoriConfig midoriConfig = MidoriConfigManager.getInstance().getConfig();

			//デバッグログが有効な場合
			if (log.isDebugEnabled()) {
				//デバッグログ出力
				log.debug("設定内容:" + midoriConfig);
			}


			//==========================================================
			//コンテナの作成と初期化

			//設定内容のコンテナのクラス名に対応するクラスオブジェクトを取得
			Class containerClass = ClassUtil.forName(midoriConfig.container);

			//コンテナクラスのクラスオブジェクトからインスタンスを生成して、フィールドに設定
			this.container = (Container) containerClass.newInstance();

			//コンテナ
			this.container.init(config);


			//==========================================================
			//プラグインの作成と初期化

			//設定内容のプラグインのMapの全ての要素に対してループをまわす
			for (Entry<String, String> entry : midoriConfig.pluginMap.entrySet()) {
				//設定内容のプラグインのMapの値(プラグインのクラス名)に対応するクラスオブジェクトを取得
				Class pluginClass = ClassUtil.forName(entry.getValue());

				//プラグインクラスのクラスオブジェクトからインスタンスを生成
				RPCPlugin plugin = (RPCPlugin) pluginClass.newInstance();

				//プラグインの初期化処理を行う
				plugin.init(config);

				//作成したプラグインをフィールドのプラグインのMapに登録
				this.pluginMap.put(entry.getKey(), plugin);
			}


			//==========================================================
			//リスティング機能の有効・無効の切り替え

			//サーブレットの初期化パラメーターからリスティング機能を有効にするかどうかのフラグを取得
			String listing = config.getInitParameter("listing");

			//「true」が指定されていた場合
			if ("true".equals(listing)) {
				//フィールドのリスティング機能を有効にするかどうかのフラグをたてる
				this.isListing = true;
			}


			//==========================================================
			//初期化処理が完了したかどうかのフラグをたてる
			this.isInit = true;
		}
		//例外処理
		catch (Exception e) {
			//デバッグログが有効な場合
			if (log.isDebugEnabled()) {
				//デバッグログ出力
				log.debug("サーブレットの初期化処理中に例外が発生しました。", e);
			}

			//捕捉した例外がサーブレット例外の場合
			if (e instanceof ServletException) {
				//捕捉した例外を元の型にキャストして投げる
				throw (ServletException) e;
			}
			//それ以外の場合
			else {
				//捕捉した例外をサーブレット例外でラッピングして投げ直す
				throw new ServletException("サーブレットの初期化処理中に例外が発生しました。", e);
			}
		}
	}

	/**
	 * 終了処理を行います。
	 */
	@Override
	public void destroy() {
		//==========================================================
		//親クラスの処理を先に呼び出す
		super.destroy();


		//==========================================================
		//コンテナの終了処理

		//コンテナがnullでない場合
		if (this.container != null) {
			try {
				//コンテナの終了処理を行う
				this.container.destroy();
			}
			//例外処理
			catch (Exception e) {
				//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
				//どうしようもないのでデバッグログを出力するだけで、例外自体はもみ消す

				//デバッグログが有効な場合
				if (log.isDebugEnabled()) {
					//デバッグログ出力
					log.debug("コンテナの終了処理中に例外が発生しました。", e);
				}
			}
		}


		//==========================================================
		//プラグインの終了処理

		//プラグインのMapがnullでない場合
		if (this.pluginMap != null) {
			//プラグインのMapの値(プラグインのインスタンス)に対してループをまわす
			for (RPCPlugin plugin : this.pluginMap.values()) {
				try {
					//プラグインの終了処理を行う
					plugin.destroy();
				}
				//例外処理
				catch (Exception e) {
					//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
					//どうしようもないのでデバッグログを出力するだけで、例外自体はもみ消す

					//デバッグログが有効な場合
					if (log.isDebugEnabled()) {
						//デバッグログ出力
						log.debug("プラグインの終了処理中に例外が発生しました。", e);
					}
				}
			}
		}
	}

	/**
	 * GETリクエストに対応する処理を行います。<br>
	 *
	 * @param request 現在のリクエスト情報
	 * @param response 現在のレスポンス情報
	 * @throws ServletException サーブレット例外
	 * @throws IOException 入出力例外
	 */
	@Override
	protected void doGet(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		//実際に処理を行うメソッドを呼び出す
		handleRequest(request, response);
	}

	/**
	 * POSTリクエストに対応する処理を行います。<br>
	 *
	 * @param request 現在のリクエスト情報
	 * @param response 現在のレスポンス情報
	 * @throws ServletException サーブレット例外
	 * @throws IOException 入出力例外
	 */
	@Override
	protected void doPost(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		//実際に処理を行うメソッドを呼び出す
		handleRequest(request, response);
	}

	/**
	 * PUTリクエストに対応する処理を行います。<br>
	 *
	 * @param request 現在のリクエスト情報
	 * @param response 現在のレスポンス情報
	 * @throws IOException 入出力例外
	 * @throws ServletException サーブレット例外
	 */
	@Override
	protected void doPut(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		//実際に処理を行うメソッドを呼び出す
		handleRequest(request, response);
	}

	/**
	 * DELETEリクエストに対応する処理を行います。<br>
	 *
	 * @param request 現在のリクエスト情報
	 * @param response 現在のレスポンス情報
	 * @throws ServletException サーブレット例外
	 * @throws IOException 入出力例外
	 */
	@Override
	protected void doDelete(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		//実際に処理を行うメソッドを呼び出す
		handleRequest(request, response);
	}


	//_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/_/
	//内部処理用

	/**
	 * GET, POST, DELETE, PUTリクエストに対応する処理をまとめて行います。<br>
	 *
	 * @param request 現在のリクエスト情報
	 * @param response 現在のレスポンス情報
	 * @throws ServletException サーブレット例外
	 * @throws IOException 入出力例外
	 */
	protected void handleRequest(HttpServletRequest request,
			HttpServletResponse response) throws ServletException, IOException {
		try {
			//==========================================================
			//サーバーのステータスのチェック

			//初期化処理が正常に完了していない場合
			if (!this.isInit) {
				//デバッグログが有効な場合
				if (log.isDebugEnabled()) {
					//デバッグログ出力
					log.debug("サーブレットの初期化処理が完了していない状態で、リクエストが行なわれました。");
				}

				//SERVICE_UNAVAILABLE(503)のレスポンスエラーを返す
				response.sendError(HttpServletResponse.SC_SERVICE_UNAVAILABLE);

				//以降の処理を行わない
				return;
			}


			//==========================================================
			//メインの処理

			//ウェブコンテキストのインスタンスを生成して、ThreadLocalに登録する
			WebContext context = WebContext.createInstance(request, response, getServletContext(), this.container);

			try {
				//リクエストの拡張パス情報を取得
				String path = context.request.getPathInfo();

				//リスティング機能が有効で、かつリクエストの拡張パス情報の部分の拡張子がnullまたは空文字列の場合
				if (this.isListing
						&& StringUtil.isBlank(FilenameUtils.getExtension(path))) {
					//リクエストの拡張パス情報自体がnullまたは空文字列、または「/」のみの場合
					//(サービスクラスが指定されていない場合)
					if (StringUtil.isBlank(path) || "/".equals(path)) {
						//サービスクラスの一覧を出力する
						listServiceClasses(context);
					}
					//それ以外の場合
					else {
						//サービスのメソッド一覧を出力する
						listServiceMethods(context);
					}
				}
				//それ以外の場合
				else {
					//リクエストの内容に応じたプラグインを取得する
					RPCPlugin plugin = getPlugin(context);

					//プラグインが取得できた場合
					if (plugin != null) {
						//プラグインにリクエストの処理から、レスポンスの出力までを委譲する
						plugin.handleRequest(context);
					}
					//それ以外の場合
					//(プラグインが取得できなかった場合)
					else {
						//デバッグログが有効な場合
						if (log.isDebugEnabled()) {
							//デバッグログ出力
							log.debug("プラグインが取得できませんでした。リクエストURL:"
									+ request.getRequestURI());
						}

						//BAD_REQUEST(400)のレスポンスエラーを返す
						response.sendError(HttpServletResponse.SC_BAD_REQUEST);

						//以降の処理を行わない
						return;
					}

				}
			}
			//終了処理
			finally {
				//ウェブコンテキストのインスタンスをThreadLocalから削除する
				WebContext.clear();
			}
		}
		//例外処理
		catch (Exception e) {
			//デバッグログが有効な場合
			if (log.isDebugEnabled()) {
				//デバッグログ出力
				log.debug("リクエストの処理中に例外が発生しました。", e);
			}

			//INTERNAL_SERVER_ERROR(500)のレスポンスエラーを返す
			response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
		}
	}

	/**
	 * リクエストの内容に応じたプラグインを取得します。<br>
	 * 取得できなかった場合は、nullを返します。<br>
	 *
	 * @param context ウェブコンテキスト
	 * @return プラグイン
	 * @throws Exception 例外
	 */
	protected RPCPlugin getPlugin(WebContext context) throws Exception {
		//リクエストの拡張パス情報を取得
		String path = context.request.getPathInfo();

		//リクエストの拡張パス情報の部分の拡張子をプラグインのタイプとして取得
		String pluginType = FilenameUtils.getExtension(path);

		//プラグインのMapからタイプに応じたプラグインを取得して返す
		//(プラグインのタイプ自体が取得できなかった場合や、タイプに対応する値がMapに登録されていない場合はnullが返される)
		return this.pluginMap.get(pluginType);
	}

	/**
	 * 指定されたリソースをクラスローダーから読み込む入力ストリームを返します。<br>
	 * リソースが見つからなかった場合はnullを返します。<br>
	 *
	 * @param name リソース名
	 * @return リソースを読み込むための入力ストリーム
	 */
	protected InputStream getResourceAsStream(String name) {
		//まずは現在のスレッドのクラスローダーから取得を試みる
		InputStream is = Thread.currentThread().getContextClassLoader().getResourceAsStream(name);

		//取得できなかった場合
		if (is == null) {
			//このクラスが属するクラスローダーから取得を試みる
			is = this.getClass().getClassLoader().getResourceAsStream(name);

			//それでも取得できなかった場合
			if (is == null) {
				//システムクラスローダーから取得を試みる
				is = ClassLoader.getSystemClassLoader().getResourceAsStream(name);
			}
		}

		//取得した入力ストリームを返す
		return is;
	}

	/**
	 * サービスクラスの一覧を出力します。<br>
	 *
	 * @param context ウェブコンテキスト
	 * @throws Exception 例外
	 */
	protected void listServiceClasses(WebContext context) throws Exception {
		//RPCの対象のサービスのリスティング情報を管理するクラスを使って、サービスクラスの一覧の情報を文字列化する
		String str = RPCServiceListingManager.getInstance().listServiceClasses();

		//取得した文字列をレスポンスのライターに出力する
		IOUtils.write(str, context.response.getWriter());
	}

	/**
	 * サービスのメソッド一覧を出力します。<br>
	 *
	 * @param context ウェブコンテキスト
	 * @throws Exception 例外
	 */
	protected void listServiceMethods(WebContext context) throws Exception {
		//リクエストの内容を元にして、呼び出し対象のサービスクラスの完全指定のクラス名を取得
		String className = RPCPluginUtil.getTargetClassName(context.request);

		//デバッグログが有効な場合
		if (log.isDebugEnabled()) {
			//デバッグログ出力
			log.debug("呼び出し対象のクラス名:" + className);
		}

		//クラス名が取得できなかった場合
		if ((className == null) || "".equals(className)) {
			//RPCの対象のサービスクラスが見つからなかった場合の例外を投げる
			throw new RPCTargetServiceNotFoundException("呼び出し対象のクラス名を取得できませんでした。");
		}

		//RPCの対象のサービスのリスティング情報を管理するクラスを使って、サービスクラスのメソッド一覧の情報を文字列化する
		String str = RPCServiceListingManager.getInstance().listServiceMethods(className, this.container);

		//取得した文字列をレスポンスのライターに出力する
		IOUtils.write(str, context.response.getWriter());
	}
}
